const axios = require('axios')
const { Transform } = require('stream')
const fs = require('fs-extra')
const _ = require('lodash')
const http = require('http')

class MultiplexedStreamTransform extends Transform {
    constructor() {
        super()
        this.buffer = Buffer.alloc(0)
    }

    _transform(chunk, encoding, callback) {
        this.buffer = Buffer.concat([this.buffer, chunk])
        let offset = 0
        while (offset < this.buffer.length) {
            if (this.buffer.length - offset < 8) {
                break
            }
            const type = this.buffer.slice(0, 4)
            const size = this.buffer.readUInt32BE(offset + 4)
            if (this.buffer.length - offset < 8 + size) {
                break
            }
            const data = this.buffer.slice(offset + 8, offset + 8 + size)
            this.push(Buffer.concat([type, data]))
            offset += 8 + size
        }
        this.buffer = this.buffer.slice(offset)
        callback()
    }
}

class ChannelAgent extends http.Agent {
    #channel
    constructor(channel) {
        super({
            keepAlive: true
        })
        this.#channel = channel
        this.#channel.setKeepAlive = this.setKeepAlive
        this.#channel.unref = this.unref
        this.#channel.setTimeout = this.setTimeout
        this.#channel.ref = this.ref
    }
    createConnection() {
        return this.#channel
    }
    setKeepAlive() {}
    unref() {}
    setTimeout() {}
    ref() {}
}

class ApiClient {
    DOCKER_MULTIPLEXED_STREAM = 'application/vnd.docker.multiplexed-stream'
    axios
    constructor(options) {
        this.axios = options.axios
    }
    async get(url, params) {
        const response = await this.axios.get(url, {
            params
        })
        return response.data
    }
    async multiplexedStreamGet(url, params, options) {
        const opts = {
            params,
            responseType: 'stream',
            headers: {
                Accept: this.DOCKER_MULTIPLEXED_STREAM
            }
        }
        Object.assign(opts, options)
        const response = await this.axios.get(url, opts)
        return response.data.pipe(new MultiplexedStreamTransform())
    }
    async octetStreamGet(url, params, options) {
        const opts = {
            params,
            responseType: 'stream'
        }
        Object.assign(opts, options)
        const response = await this.axios.get(url, opts)
        return response.data
    }
    async post(url, data, params) {
        const response = await this.axios.post(url, data, {
            params: params
        })
        return response.data
    }
    async multiplexedStreamPost(url, data, params) {
        const opts = {
            params,
            responseType: 'stream',
            headers: {
                Accept: this.DOCKER_MULTIPLEXED_STREAM
            },
            data,
            method: 'POST',
            url
        }
        const response = await this.axios.request(opts)
        return response.data.pipe(new MultiplexedStreamTransform())
    }
    async streamPost(url, data, params) {
        const opts = {
            params,
            responseType: 'stream',
            data,
            method: 'POST',
            url
        }
        const response = await this.axios.request(opts)
        return response.data
    }
    async remove(url, params) {
        const response = await this.axios.delete(url, {
            params
        })
        return response.data
    }
}

class ContainerApi extends ApiClient {
    async containers() {
        return await this.get('/containers/json', {
            all: true
        })
    }
    async create(name, platform) {
        const data = {
            Cmd: ['nginx', '-g', 'daemon off;'],
            Image: 'nginx:latest',
            HostConfig: {
                PortBindings: {
                    '80/tcp': [
                        {
                            HostPort: '8080'
                        }
                    ]
                }
            }
        }
        return this.post('/containers/create', data, {
            name,
            platform
        })
    }
    async start(idOrName, detachKeys) {
        return this.post(
            '/containers/' + idOrName + '/start',
            {},
            {
                detachKeys
            }
        )
    }
    async stop(idOrName, signal, t) {
        return this.post(
            '/containers/' + idOrName + '/stop',
            {},
            {
                signal,
                t
            }
        )
    }
    async kill(idOrName, signal) {
        return this.post(
            '/containers/' + idOrName + '/kill',
            {},
            {
                signal
            }
        )
    }
    async restart(idOrName, signal, t) {
        return this.post(
            '/containers/' + idOrName + '/restart',
            {},
            {
                signal,
                t
            }
        )
    }
    async pause(idOrName) {
        return this.post('/containers/' + idOrName + '/pause')
    }
    async unpause(idOrName) {
        return this.post('/containers/' + idOrName + '/unpause')
    }
    async remove(idOrName, v, force, link) {
        return super.remove('/containers/' + idOrName, {
            v,
            force,
            link
        })
    }
    async inspect(idOrName, hasContainerSize = true) {
        return super.get('/containers/' + idOrName + '/json', {
            size: hasContainerSize
        })
    }
    async logs(idOrName, callback) {
        const params = {
            stdout: true,
            stderr: true
        }
        const stream = await this.multiplexedStreamGet('/containers/' + idOrName + '/logs', params)
        return await new Promise((resolve, reject) => {
            stream.on('data', buffer => {
                const type = buffer.readUint8(0)
                const data = buffer.slice(4).toString()
                switch (type) {
                    case 1:
                        callback?.('STD_OUT', data)
                        break
                    case 2:
                        callback?.('STD_ERR', data)
                        break
                }
            })
            stream.on('end', resolve)
            stream.on('error', reject)
        })
    }
    async processes(idOrName, ps_args = '-ef') {
        return this.get('/containers/' + idOrName + '/top', {
            ps_args
        })
    }
    async export(idOrName, clientFile) {
        const stream = await this.octetStreamGet('/containers/' + idOrName + '/export')
        const fileStream = fs.createWriteStream(clientFile)
        stream.pipe(fileStream)
        return await new Promise((resolve, reject) => {
            fileStream.on('end', resolve)
            fileStream.on('error', reject)
        })
    }
    async attach(idOrName, callback) {
        const stream = await this.multiplexedStreamPost(
            '/containers/' + idOrName + '/attach',
            {},
            {
                // detachKeys: '',
                logs: true,
                stream: true,
                stdin: true,
                stdout: true,
                stderr: true
            }
        )
        return await new Promise((resolve, reject) => {
            stream.on('data', buffer => {
                const type = buffer.readUint8(0)
                const data = buffer.slice(4).toString()
                switch (type) {
                    case 1:
                        callback?.('STD_OUT', data)
                        break
                    case 2:
                        callback?.('STD_ERR', data)
                        break
                }
            })
            stream.on('end', resolve)
            stream.on('error', reject)
        })
    }
}

class ImageApi extends ApiClient {
    async images() {
        return await this.get('/images/json', {
            all: true
        })
    }
    async create(fromImage, repo) {
        const data = {}
        return this.post('/images/create', data, {
            params: {
                fromImage,
                repo
            }
        })
    }
    async remove(idOrName, v, force, noprune) {
        return super.remove('/images/' + idOrName, {
            v,
            force,
            noprune
        })
    }
    async inspect(idOrName) {
        return super.get('/images/' + idOrName + '/json')
    }
    async export(idOrName) {
        const stream = await this.octetStreamGet('/images/' + idOrName + '/get')
        const fileStream = fs.createWriteStream(clientFile)
        stream.pipe(fileStream)
        return await new Promise((resolve, reject) => {
            fileStream.on('end', resolve)
            fileStream.on('error', reject)
        })
    }
    async search(term, limit, filters) {
        return super.get('/images/search', {
            term,
            limit,
            filters
        })
    }
    async history(idOrName) {
        return super.get('/images/' + idOrName + '/history')
    }
}

class ServiceApi extends ApiClient {
    async logs(idOrName) {
        return await this.get('/services/' + idOrName + '/logs')
    }
}

class SystemApi extends ApiClient {
    async info() {
        return await this.get('/info')
    }
    async version() {
        return await this.get('/version')
    }
    async ping() {
        return await this.get('/_ping')
    }
}

class ExecApi extends ApiClient {
    async create(idOrName, Cmd, Env) {
        const opts = {
            AttachStdin: true,
            AttachStdout: true,
            AttachStderr: true,
            DetachKeys: 'ctrl-p,ctrl-q',
            Tty: true,
            Cmd: Cmd,
            Env: Env
        }
        return await this.post('/containers/' + idOrName + '/exec', opts)
    }
    async start(idOrName, detach = true) {
        const opts = {
            Detach: detach,
            Tty: true,
            ConsoleSize: [80, 64]
        }
        return await this.streamPost('/exec/' + idOrName + '/start', opts)
    }
}

class Docker {
    Container
    Image
    Service
    System
    Exec
    axios
    opts = {
        baseURL: '',
        headers: {
            'Content-Type': 'application/json'
        }
    }
    constructor(options) {
        Object.assign(this.opts, options)
        if (this.opts.channel) {
            this.opts.httpAgent = new ChannelAgent(this.opts.channel)
        }
        this.opts.axios = axios.create(this.opts)
        this.Container = new ContainerApi(this.opts)
        this.Image = new ImageApi(this.opts)
        this.Service = new ServiceApi(this.opts)
        this.System = new SystemApi(this.opts)
        this.Exec = new ExecApi(this.opts)
    }
}
module.exports = {
    Docker,
    ContainerApi,
    ServiceApi,
    ImageApi,
    SystemApi
}
